/*
 * MIT License
 *
 * Copyright (c) 2024-2048 冰羽
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package cn.star.framework.extend.schema.extract.internal;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.star.framework.extend.core.ExtractionContextTuple;
import cn.star.framework.extend.type.TableType;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import lombok.Getter;
import org.hibernate.boot.model.TruthValue;
import org.hibernate.boot.model.naming.DatabaseIdentifier;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.relational.QualifiedTableName;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.dialect.DB2Dialect;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.internal.util.config.ConfigurationHelper;
import org.hibernate.tool.schema.extract.internal.AbstractInformationExtractorImpl;
import org.hibernate.tool.schema.extract.internal.InformationExtractorJdbcDatabaseMetaDataImpl;
import org.hibernate.tool.schema.extract.spi.ColumnInformation;
import org.hibernate.tool.schema.extract.spi.ExtractionContext;
import org.hibernate.tool.schema.extract.spi.TableInformation;

/**
 * NativeInfoExtractorImpl<br>
 *
 * @author zhaoweiping
 *     <p style='color: red'>Created on 2024-09-23 11:06:00
 * @since 3.0.0
 */
public class NativeInfoExtractorImpl extends InformationExtractorJdbcDatabaseMetaDataImpl {

  private final String[] tableTypes;
  @Getter private final ExtractionContextTuple extractionContextTuple;

  public NativeInfoExtractorImpl(ExtractionContextTuple tuple) {
    super(tuple.getExtractionContext());
    this.extractionContextTuple = tuple;
    ExtractionContext extractionContext = tuple.getExtractionContext();
    // AbstractInformationExtractorImpl#constructor define
    final Dialect dialect = extractionContext.getJdbcEnvironment().getDialect();
    final ConfigurationService configService =
        extractionContext.getServiceRegistry().getService(ConfigurationService.class);
    final List<String> tableTypesList = new ArrayList<>();
    tableTypesList.add("TABLE");
    tableTypesList.add("VIEW");
    if (ConfigurationHelper.getBoolean(
        AvailableSettings.ENABLE_SYNONYMS, configService.getSettings(), false)) {
      if (dialect instanceof DB2Dialect) {
        tableTypesList.add("ALIAS");
      }
      tableTypesList.add("SYNONYM");
    }
    dialect.augmentRecognizedTableTypes(tableTypesList);

    this.tableTypes = tableTypesList.toArray(new String[0]);
  }

  /**
   * 使用{@link AbstractInformationExtractorImpl#addColumns(TableInformation)}支持设置列注释<br>
   * 而{@link InformationExtractorJdbcDatabaseMetaDataImpl#addColumns(TableInformation)}不支持设置列注释
   */
  @Override
  protected void addColumns(TableInformation tableInformation) {
    final QualifiedTableName tableName = tableInformation.getName();
    final Identifier catalog = tableName.getCatalogName();
    final Identifier schema = tableName.getSchemaName();

    final String catalogFilter = catalog == null ? "" : catalog.getText();
    final String schemaFilter = schema == null ? "" : schema.getText();

    try {
      processColumnsResultSet(
          catalogFilter,
          schemaFilter,
          tableName.getTableName().getText(),
          "%",
          resultSet -> {
            while (resultSet.next()) {
              addExtractedColumnInformation(tableInformation, resultSet);
            }
            return null;
          });

    } catch (SQLException e) {
      throw convertSQLException(e, "Error accessing tables metadata");
    }
  }

  @Override
  protected void addExtractedColumnInformation(
      TableInformation tableInformation, ResultSet resultSet) throws SQLException {
    final ColumnInformation columnInformation =
        new ColumnInformationEnhance(
            tableInformation,
            DatabaseIdentifier.toIdentifier(resultSet.getString(getResultSetColumnNameLabel())),
            resultSet.getInt(getResultSetSqlTypeCodeLabel()),
            new StringTokenizer(resultSet.getString(getResultSetTypeNameLabel()), "() ")
                .nextToken(),
            resultSet.getInt(getResultSetColumnSizeLabel()),
            resultSet.getInt(getResultSetDecimalDigitsLabel()),
            interpretTruthValue(resultSet.getString(getResultSetIsNullableLabel())),
            resultSet.getString(getResultSetRemarksLabel()));

    tableInformation.addColumn(columnInformation);
  }

  private TruthValue interpretTruthValue(String nullable) {
    String yes = "YES", no = "NO";
    if (yes.equalsIgnoreCase(nullable)) {
      return TruthValue.TRUE;
    } else if (no.equalsIgnoreCase(nullable)) {
      return TruthValue.FALSE;
    }
    return TruthValue.UNKNOWN;
  }

  private String[] getTableTypes(TableType... types) {
    Set<String> list = new HashSet<>();
    TableType[] tableTypes = ArrayUtil.isNotEmpty(types) ? types : TableType.values();
    for (TableType tableType : tableTypes) {
      String[] values = tableType.getValues();
      if (ArrayUtil.isNotEmpty(values)) {
        list.addAll(CollectionUtil.toList(values));
      }
    }

    return list.isEmpty() ? this.tableTypes : ArrayUtil.toArray(list, String.class);
  }

  /** {@link AbstractInformationExtractorImpl#extractTableName} */
  private QualifiedTableName extractTableName(ResultSet resultSet) throws SQLException {
    final String incomingCatalogName = resultSet.getString(getResultSetCatalogLabel());
    final String incomingSchemaName = resultSet.getString(getResultSetSchemaLabel());
    final String incomingTableName = resultSet.getString(getResultSetTableNameLabel());

    final DatabaseIdentifier catalog = DatabaseIdentifier.toIdentifier(incomingCatalogName);
    final DatabaseIdentifier schema = DatabaseIdentifier.toIdentifier(incomingSchemaName);
    final DatabaseIdentifier table = DatabaseIdentifier.toIdentifier(incomingTableName);

    return new QualifiedTableName(catalog, schema, table);
  }

  /**
   * 获取数据库中的表名
   *
   * @param catalog catalog
   * @param schema schema
   * @param types 表类型
   * @return 表名列表
   */
  public List<String> getNativeTableNames(String catalog, String schema, TableType... types) {
    try {
      String[] tableTypes = this.getTableTypes(types);
      return this.processTableResultSet(
          catalog,
          schema,
          "%",
          tableTypes,
          resultSet -> {
            List<String> tableNames = new ArrayList<>();
            while (resultSet.next()) {
              QualifiedTableName qualifiedTableName = this.extractTableName(resultSet);
              String text = qualifiedTableName.getTableName().getText();
              tableNames.add(text);
            }
            return tableNames;
          });
    } catch (SQLException sqlException) {
      throw convertSQLException(sqlException, "Error accessing table metadata");
    }
  }

  /**
   * 获取数据库表对应的列名
   *
   * @param catalog catalog
   * @param schema schema
   * @param table table
   * @return 列名列表
   */
  public List<String> getNativeTableColumns(String catalog, String schema, String table) {
    try {
      return this.processColumnsResultSet(
          catalog,
          schema,
          table,
          "%",
          resultSet -> {
            List<String> columnNames = new ArrayList<>();
            while (resultSet.next()) {
              String name = resultSet.getString(this.getResultSetColumnNameLabel());
              columnNames.add(name);
            }
            return columnNames;
          });
    } catch (SQLException sqlException) {
      throw convertSQLException(sqlException, "Error accessing table column metadata");
    }
  }

  /**
   * {@link #getTable(Identifier, Identifier, Identifier)}
   *
   * @param catalog catalog
   * @param schema schema
   * @param table 表名
   * @param types 表类型
   * @return 表结构
   */
  public TableInformation getTableInformation(
      String catalog, String schema, String table, TableType... types) {
    // TODO 表类型暂未生效
    return this.getTable(wrapper(catalog), wrapper(schema), wrapper(table));
  }

  public static String wrapper(Identifier identifier) {
    return identifier == null ? null : identifier.getText();
  }

  public static Identifier wrapper(String text) {
    return Identifier.toIdentifier(text);
  }

  public static Identifier wrapper(String text, boolean quote) {
    return Identifier.toIdentifier(text, quote);
  }

  public static Identifier wrapper(String text, boolean quote, boolean quoteOnNonIdentifierChar) {
    return Identifier.toIdentifier(text, quote, quoteOnNonIdentifierChar);
  }

  @Deprecated
  public boolean isClosed() {
    try {
      // 不能使用这个来判断连接是否关闭return
      // this.getExtractionContext().getJdbcConnection().isClosed();
      // 因为由于HIKARI数据库连接池导致拿到的连接对象是代理对象，好像永远不会关闭似的;
      // 但是真正的连接可能会由于数据库服务器端关闭连接而程序的数据库连接链接池不能感知的问题;
      // 对于这个获取表结构的连接来说，连接一直在meta中，导致连接池好像也不能知道是不是断了;
      // 若是查询数据库的连接，可以使用配置TestQuery和ValidateTimeout来配置探活
      ExtractionContext context = this.getExtractionContext();
      // 都是同一个对象，所以这个才是数据库结构信息获取的正确连接
      DatabaseMetaData meta = context.getJdbcDatabaseMetaData();
      // 而 Connection connection = context.getJdbcConnection().getMetaData(); 获取的都是一个都是一个新的对象

      return meta.getConnection().isClosed()
          // 放置数据库连接池HIKARI代理产生的代理连接
          || meta.getConnection().getMetaData().getConnection().isClosed();
    } catch (Exception e) {
      return true;
    }
  }
}
